SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
4.9 KB · 104 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { EventRow } from "@/components/market/event-row";4import { InstrumentTable } from "@/components/market/instrument-table";5import { Empty, Page, PageHeader, Section, Stat } from "@/components/ui/section";6import { StatusBadge } from "@/components/ui/status-badge";7import { api } from "@/lib/api";8import type { Breadth, Country, Exchange, ExchangeStatus, InstrumentWithQuote, MarketEvent } from "@/lib/types";910interface Detail {11  country: Country;12  exchanges: Array<Exchange & { status: ExchangeStatus }>;13  indices: InstrumentWithQuote[];14  rates: InstrumentWithQuote[];15  fx: InstrumentWithQuote[];16  equities: InstrumentWithQuote[];17  breadth: Breadth | null;18  events: MarketEvent[];19  instruments_tracked: number;20}2122export const dynamic = "force-dynamic";2324export async function generateMetadata({ params }: { params: Promise<{ code: string }> }): Promise<Metadata> {25  const { code } = await params;26  try {27    const d = await api<Detail>(`/v1/countries/${encodeURIComponent(code)}`);28    return { title: `${d.country.name} — markets`, description: `${d.country.name}: exchanges, indices, rates, ${d.country.currency ?? "currency"} pairs and the equities Market Atlas observes.`, alternates: { canonical: `/countries/${d.country.code}` } };29  } catch {30    return { title: "Country" };31  }32}3334export default async function CountryPage({ params }: { params: Promise<{ code: string }> }) {35  const { code } = await params;36  const d = await api<Detail>(`/v1/countries/${encodeURIComponent(code)}`);37  const c = d.country;38  const open = d.exchanges.filter((e) => e.status.state === "OPEN").length;39  return (40    <Page wide>41      <PageHeader42        kicker={43          <Link href="/countries" className="hover:text-ink">44            Countries · {c.region}45          </Link>46        }47        title={c.name}48        lead={`${c.currency ? `Currency ${c.currency} · ` : ""}${d.exchanges.length} venue${d.exchanges.length === 1 ? "" : "s"} in the atlas · ${d.instruments_tracked.toLocaleString("en-US")} instruments tracked.`}49      />50      <div className="grid grid-cols-2 gap-4 rounded-md border border-rule bg-surface px-4 py-4 sm:grid-cols-4">51        <Stat label="Venues open" value={`${open} / ${d.exchanges.length}`} />52        <Stat label="Indices" value={d.indices.length} sub="with canonical level" />53        <Stat label="Rates & yields" value={d.rates.length} sub="official series" />54        <Stat label="Equity breadth" value={d.breadth ? `${d.breadth.advancers} ▲ ${d.breadth.decliners} ▼` : "—"} sub={d.breadth ? `${d.breadth.instruments} quoted` : "no quoted equities"} />55      </div>56      <Section title="Exchanges" href="/exchanges">57        {d.exchanges.length ? (58          <ul className="grid grid-cols-1 [&>*]:min-w-0 gap-2 sm:grid-cols-2 lg:grid-cols-3">59            {d.exchanges.map((e) => (60              <li key={e.id} className="rounded-md border border-rule bg-surface px-3 py-2.5">61                <Link href={`/exchanges/${e.id}`} className="font-medium hover:underline">62                  {e.name}63                </Link>64                <div className="mt-1 flex items-center gap-2 text-xs text-ink-3">65                  <StatusBadge status={e.status.state} /> <span className="mono">{e.status.localTime}</span> local · {e.mic ?? e.id}66                </div>67              </li>68            ))}69          </ul>70        ) : (71          <Empty>No venue of {c.name} is in the atlas yet.</Empty>72        )}73      </Section>74      {d.indices.length > 0 && (75        <Section title="Indices" href="/indices">76          <InstrumentTable rows={d.indices} compact showExchange={false} />77        </Section>78      )}79      <div className="grid grid-cols-1 [&>*]:min-w-0 gap-6 lg:grid-cols-2">80        <Section title="Rates & yields" href="/rates" hint="percent · official">81          <InstrumentTable rows={d.rates} compact showExchange={false} defaultSort="symbol" emptyText={`No official rate series for ${c.name} yet.`} />82        </Section>83        <Section title={`${c.currency ?? "Currency"} pairs`} href="/forex" hint="reference rates">84          <InstrumentTable rows={d.fx} compact showExchange={false} defaultSort="symbol" emptyText="No currency pairs yet." />85        </Section>86      </div>87      <Section title="Equities" href={`/stocks`} hint="most active among tracked names">88        <InstrumentTable rows={d.equities} compact emptyText={`No quoted equities for ${c.name} yet — the instrument master lists them but no source publishes their prices.`} />89      </Section>90      <Section title="Events">91        {d.events.length ? (92          <ul className="rounded-md border border-rule bg-surface px-3">93            {d.events.map((e) => (94              <EventRow key={e.id} e={e} />95            ))}96          </ul>97        ) : (98          <Empty>No events touching {c.name} instruments yet.</Empty>99        )}100      </Section>101    </Page>102  );103}104